﻿// <copyright file="DataTableExtensions.cs" company="DotNetLib">
//
// .netlib: http://www.codeplex.com/DotNetLib
//
// This code is released under the New BSD License.
// A copy of this license is available at:
// http://www.codeplex.com/DotNetLib/license
//
// </copyright>

namespace DotNetLib.Data
{
  using System;
  using System.Collections.Generic;
  using System.Data;
  using System.Globalization;
  using System.IO;

  /// <summary>
  /// Extension methods for a <see cref="DataTable"/>.
  /// </summary>
  public static class DataTableExtensions
  {
    /// <summary>
    /// Reads CSV data into the <see cref="DataTable"/> using the specified file name.
    /// </summary>
    /// <param name="table">The table to read the CSV data into.</param>
    /// <param name="fileName">The name of the file from which to read the data.</param>
    /// <param name="hasHeader">Whether the CSV document has a header row.</param>
    public static void ReadCsv(this DataTable table, string fileName, bool hasHeader)
    {
      using (StreamReader reader = new StreamReader(fileName))
      {
        ReadCsv(table, reader, hasHeader);
      }
    }

    /// <summary>
    /// Reads CSV data into the <see cref="DataTable"/> using the specified <see cref="Stream"/>.
    /// </summary>
    /// <param name="table">The table to read the CSV data into.</param>
    /// <param name="stream">The stream from which to read the data.</param>
    /// <param name="hasHeader">Whether the CSV document has a header row.</param>
    public static void ReadCsv(this DataTable table, Stream stream, bool hasHeader)
    {
      using (StreamReader reader = new StreamReader(stream))
      {
        ReadCsv(table, reader, hasHeader);
      }
    }

    /// <summary>
    /// Reads CSV data into the <see cref="DataTable"/> using the specified <see cref="TextReader"/>.
    /// </summary>
    /// <param name="table">The table to read the CSV data into.</param>
    /// <param name="reader">The reader from which to read the data.</param>
    /// <param name="hasHeader">Whether the CSV document has a header row.</param>
    public static void ReadCsv(this DataTable table, TextReader reader, bool hasHeader)
    {
      string line = reader.ReadLine();
      if (string.IsNullOrEmpty(line))
      {
        return;
      }

      string[] parts = ReadParts(line);

      if (hasHeader)
      {
        foreach (string part in parts)
        {
          table.Columns.Add(part, typeof(string));
        }

        line = reader.ReadLine();
      }
      else
      {
        foreach (string part in parts)
        {
          table.Columns.Add().DataType = typeof(string);
        }
      }

      while (!string.IsNullOrEmpty(line))
      {
        parts = ReadParts(line);

        DataRow row = table.NewRow();
        for (int index = 0; index < parts.Length; ++index)
        {
          row[index] = parts[index];
        }
        table.Rows.Add(row);

        line = reader.ReadLine();
      }
    }

    /// <summary>
    /// Writes the <see cref="DataTable"/> to a CSV file using the specified file name.
    /// </summary>
    /// <param name="table">The table that contains the data to write.</param>
    /// <param name="fileName">The name of the file where the data should be written.</param>
    /// <param name="hasHeader">Whether the CSV document should have a header row.</param>
    public static void WriteCsv(this DataTable table, string fileName, bool hasHeader)
    {
      using (StreamWriter writer = new StreamWriter(fileName))
      {
        WriteCsv(table, writer, hasHeader);
      }
    }

    /// <summary>
    /// Reads CSV data into the <see cref="DataTable"/> using the specified <see cref="Stream"/>.
    /// </summary>
    /// <param name="table">The table that contains the data to write.</param>
    /// <param name="stream">The stream where the data ahould be written.</param>
    /// <param name="hasHeader">Whether the CSV document should have a header row.</param>
    public static void WriteCsv(this DataTable table, Stream stream, bool hasHeader)
    {
      using (StreamWriter writer = new StreamWriter(stream))
      {
        WriteCsv(table, writer, hasHeader);
      }
    }

    /// <summary>
    /// Reads CSV data into the <see cref="DataTable"/> using the specified <see cref="TextReader"/>.
    /// </summary>
    /// <param name="table">The table that contains the data to write.</param>
    /// <param name="writer">The <see cref="TextWriter"/> where the data ahould be written.</param>
    /// <param name="hasHeader">Whether the CSV document should have a header row.</param>
    public static void WriteCsv(this DataTable table, TextWriter writer, bool hasHeader)
    {
      if (hasHeader)
      {
        for (int index = 0; index < table.Columns.Count; ++index)
        {
          if (index > 0)
          {
            writer.Write(",");
          }

          writer.Write(QuoteText(table.Columns[index].ColumnName));
        }

        writer.WriteLine();
      }

      for (int rowIndex = 0; rowIndex < table.Rows.Count; ++rowIndex)
      {
        for (int colIndex = 0; colIndex < table.Columns.Count; ++colIndex)
        {
          if (colIndex > 0)
          {
            writer.Write(",");
          }

          writer.Write(QuoteText(Convert.ToString(table.Rows[rowIndex][colIndex], CultureInfo.InvariantCulture)));
        }

        writer.WriteLine();
      }
    }

    /// <summary>
    /// Get the differences between this <see cref="DataTable"/> and the specified <see cref="DataTable"/>.
    /// </summary>
    /// <param name="original">The table that represents the original set of data.</param>
    /// <param name="table">The table that represents the new set of data.</param>
    /// <returns>A table that represents the differences between the sets of data.</returns>
    public static DataTable GetDifferences(this DataTable original, DataTable table)
    {
      if (original == null)
      {
        throw new ArgumentNullException("original");
      }

      if (table == null)
      {
        throw new ArgumentNullException("table");
      }

      if (original.Columns.Count != table.Columns.Count)
      {
        throw new ArgumentOutOfRangeException("table", "Columns do not match.");
      }

      if (original.PrimaryKey == null || original.PrimaryKey.Length != 1 || table.PrimaryKey == null || table.PrimaryKey.Length != 1)
      {
        throw new ArgumentOutOfRangeException("original", "Tables must have single column primary keys.");
      }

      DataView originalView = new DataView(original, null, original.PrimaryKey[0].ColumnName, DataViewRowState.CurrentRows);
      DataView tableView = new DataView(table, null, table.PrimaryKey[0].ColumnName, DataViewRowState.CurrentRows);
      int originalIndex = 0;
      int tableIndex = 0;

      DataTable result = original.Copy();

      while (true)
      {
        DataRow originalRow = null;
        DataRow tableRow = null;
        if (originalIndex < originalView.Count)
        {
          originalRow = originalView[originalIndex].Row;
        }

        if (tableIndex < tableView.Count)
        {
          tableRow = tableView[tableIndex].Row;
        }

        if (originalRow == null && tableRow == null)
        {
          // ran out of rows, done
          break;
        }
        else if (originalRow == null)
        {
          // ran out of original, this row is inserted
          result.Rows.Add(tableRow.ItemArray);
          tableIndex++;
        }
        else if (tableRow == null)
        {
          // ran out of new, this row is deleted
          result.Rows.Find(originalRow[original.PrimaryKey[0]]).Delete();
          originalIndex++;
        }
        else
        {
          IComparable originalKey = (IComparable)originalRow[original.PrimaryKey[0]];
          IComparable tableKey = (IComparable)tableRow[table.PrimaryKey[0]];

          int compareResult = originalKey.CompareTo(tableKey);
          if (compareResult < 0)
          {
            // rows don't match, original is smaller, this row is deleted
            result.Rows.Find(originalRow[original.PrimaryKey[0]]).Delete();
            originalIndex++;
          }
          else if (compareResult > 0)
          {
            // rows don't match, new is smaller, this row is inserted
            result.Rows.Add(tableRow.ItemArray);
            tableIndex++;
          }
          else
          {
            DataRow resultRow = result.Rows.Find(originalRow[original.PrimaryKey[0]]);
            bool resultRowDirty = false;
            for (int columnIndex = 0; columnIndex < original.Columns.Count; ++columnIndex)
            {
              if (originalRow.IsNull(columnIndex) && tableRow.IsNull(columnIndex))
              {
                continue;
              }
              else if (originalRow.IsNull(columnIndex) || tableRow.IsNull(columnIndex))
              {
                resultRow.ItemArray = tableRow.ItemArray;
                resultRowDirty = true;
                break;
              }
              else
              {
                IComparable originalValue = (IComparable)originalRow[columnIndex];
                IComparable tableValue = (IComparable)tableRow[columnIndex];

                if (originalValue.CompareTo(tableValue) != 0)
                {
                  resultRow.ItemArray = tableRow.ItemArray;
                  resultRowDirty = true;
                  break;
                }
              }
            }

            if (!resultRowDirty)
            {
              result.Rows.Remove(result.Rows.Find(originalRow[original.PrimaryKey[0]]));
            }

            originalIndex++;
            tableIndex++;
          }
        }
      }

      return result;
    }

    private static string[] ReadParts(string line)
    {
      List<string> parts = new List<string>();

      bool inQuotes = false;
      int partIndex = 0;
      int endPartIndex = 0;
      for (int index = 0; index < line.Length; ++index)
      {
        if (inQuotes)
        {
          if (line[index] == '"')
          {
            endPartIndex = index - 1;
            inQuotes = false;
          }
          else
          {
            endPartIndex = index;
          }
        }
        else
        {
          if (line[index] == '"')
          {
            partIndex = index + 1;
            endPartIndex = partIndex;
            inQuotes = true;
          }
          else if (line[index] == ',')
          {
            parts.Add(line.Substring(partIndex, endPartIndex - partIndex + 1));
            partIndex = index + 1;
            endPartIndex = partIndex;
          }
          else
          {
            endPartIndex = index;
          }
        }
      }
      parts.Add(line.Substring(partIndex, endPartIndex - partIndex + 1));

      return parts.ToArray();
    }

    /// <summary>
    /// Called to quote text so the CSV is well formatted.
    /// </summary>
    /// <param name="value">The value to quote.</param>
    /// <returns>The quoted text.</returns>
    private static string QuoteText(object value)
    {
      if (value == null || Convert.IsDBNull(value))
      {
        return string.Empty;
      }

      string strValue = value.ToString();
      if (strValue.Contains(","))
      {
        return "\"" + strValue + "\"";
      }
      else
      {
        return strValue;
      }
    }
  }
}
